package prefix.platform.email;

import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

import models.Msg;
import models.MsgDetail;

import play.Logger;
import play.Logger.ALogger;
import play.db.jpa.JPA;
import prefix.platform.Constants;
import prefix.platform.email.QueuedEmailEngine.ProcFlag;
import prefix.util.JPAUtil;
import service.ActorService;

public class AbstractEmailEngine {

    // Cache SMTP sessions
    private static Map<String, Session> sessions = new HashMap<String, Session>();

    public static enum SEVERITY {
        ERROR,
        WARN,
        INFO;
    }

    //    all the configuration variable here, will move to configuration soon
    public final static String FROM_ADDRESS = "306044084@qq.com";

    public final static String FROM_NAME = "是我是我";

    public final static String INSTANCE = "DEV";

    public final static String EMAIL_LIST_NOTIFY_ADMIN = "wynewsjobs@gmail.com";

    public final static String ROOT_URL = "";

    public final static boolean EMAIL_SENDING_ENABLE = true;

    public final static String SMTP_SERVER = "smtp.qq.com";

    public final static String SMTP_PORT = "25";

    public final static int SMTP_TIMEOUT = 10000;

    public final static String SMTP_USERNAME = "306044084@qq.com";

    public final static String SMTP_PASSWORD = "wytc@6219895";
    
    public final static int EMAIL_MAX_TRY_CNT = 3;

    public static enum EmailType {
        NONE(false, "none"), // 0 - 
        ADMIN(false, "Admin"); // 1 - 

        private boolean userVisible;

        private String label;

        private EmailType(boolean isUserV, String s) {
            label = s;
            userVisible = isUserV;
        }

        public boolean isUserVisible() {
            return this.userVisible;
        }

        public String getLabel() {
            return label;
        }
    }

    /**
     * Send message to the given actor using the speficied email template filled
     * with the given variables
     */
    public static void sendTemplatedMessage(String toAddress, ActorService recipient, Map<String, String> vars,
            EmailTemplate emailTemplate, ProcFlag flag, List<String> attachementsFileNames) {

        if (vars == null) {
            vars = new HashMap<String, String>();
        }

        // Utility vars
        vars.put("$EMAIL$", toAddress);
        vars.put("$URL_ROOT$", AbstractEmailEngine.ROOT_URL);
        if (recipient != null) {
            if (recipient.getDBActor().getHonourific() != null) {
                vars.put("$HONOURIFIC$", recipient.getDBActor().getHonourific().name());
            }
            vars.put("$USERNAME$", recipient.getDBActor().getUsername());
            if (recipient.getDBActor().getFullName() != null) {
                vars.put("$REAL_NAME$", recipient.getDBActor().getFullName());
            }
        }

        // Build message from template
        Message message = emailTemplate.buildMessage(toAddress, vars, null);

        EmailTemplate.EmailParams params = emailTemplate.getMessageParams(vars);

        if (params != null && message != null) {

            String subject = "";
            String content = "";
            try {
                subject = message.getSubject();
                content = message.getContent().toString();
            } catch (Exception e) {
                // Something went wrong when reading message, should never happen
                e.printStackTrace();
            }

            sendMessage(toAddress, recipient, emailTemplate.getType(), params.getCc(), params.getBcc(),
                    params.getFromAddress(), params.getFromName(), subject, content, params.getReplyTo(),
                    attachementsFileNames, Msg.Type.NONE, Msg.Transport.EMAIL, flag);
        }
    }

    //------------------------------------------------------------------------------------------------ 
    /**
     * Main messaging method. Use this method to send a message using the given
     * transport settings. Non-blocking method, a thread is spawned to take care
     * of the message submission
     */
    public static int MAX_THREADS = 100; // Maximum amount of threads running at the same time

    private static int runningThreadsCount = 0; // Running threads count

    public static void sendMessage(String toAddress, ActorService recipient, EmailType emailType, String cc,
            String bcc, String fromAddress, String fromName, String subject, String content, String replyTo,
            List<String> attachementsFileNames, Msg.Type messageType, Msg.Transport messageTransport, ProcFlag procFlag) {

        // Create final vars to pass to the thread
        final ActorService _recipient = recipient;
        final String _toAddress = toAddress;
        final EmailType _emailType = emailType;
        final String _cc = cc;
        final String _bcc = bcc;
        final String _fromAddress = fromAddress;
        final String _fromName = fromName;
        final String _subject = subject;
        final String _content = content;
        final String _replyTo = replyTo;
        final List<String> _attachementsFileNames = attachementsFileNames;
        final Msg.Type _messageType = messageType;
        final Msg.Transport _messageTransport = messageTransport;
        final ProcFlag _procFlag = procFlag;

        if (runningThreadsCount < MAX_THREADS) {
            // Spawn thread to perform non-blocking mail sending
            runningThreadsCount++;

            Logger.of(Constants.APPLICATION).info("Spawining email sending thread #" + runningThreadsCount);

            Thread thread = new Thread("Send Queue Mail thread #" + runningThreadsCount) {
                public void run() {
                    try {
                        _sendMessage(_toAddress, _recipient, _emailType, _cc, _bcc, _fromAddress, _fromName, _subject,
                                _content, _replyTo, _attachementsFileNames, _messageType, _messageTransport, _procFlag);
                    } catch (Throwable t) {
                        Logger.of(Constants.APPLICATION).error("Errow while sending queue mail asynchronically", t);
                    }
                    runningThreadsCount--;
                }
            };

            thread.start();
        } else {
            Logger.info("Not spawining email sending thread, reached max thread count");

            System.out.println("WARNING: " + runningThreadsCount + " threads sending emails simultaneously");

            // Perform blocking call to prevent consuming too many resources
            _sendMessage(_toAddress, _recipient, _emailType, _cc, _bcc, _fromAddress, _fromName, _subject, _content,
                    _replyTo, _attachementsFileNames, _messageType, _messageTransport, _procFlag);
        }
    }

    //------------------------------------------------------------------------------------------------ 
    /**
     * Normalize target email address
     * 
     * @param address
     * @return
     */
    private static String normalizeToAddress(String toAddress) {

        toAddress = toAddress.trim();
        // In demo mode, send all emails to qatester@sinolending.com

        return toAddress;
    }

    //------------------------------------------------------------------------------------------------ 
    private static void _sendMessage(String toAddress, ActorService recipient, EmailType emailType, String cc,
            String bcc, String fromAddress, String fromName, String subject, String content, String replyTo,
            List<String> attachementsFileNames, Msg.Type messageType, Msg.Transport messageTransport, ProcFlag procFlag) {

        Msg.Status status = Msg.Status.PENDING;
        String error = null;
        Date sentDate = Calendar.getInstance().getTime(); // Now

        // Normalize address when sending messages to users
        if (recipient != null) {
            // Normalize to address
            toAddress = normalizeToAddress(toAddress);
        }

        // Send message right away
        if (procFlag == ProcFlag.IMMEDIATE || procFlag == ProcFlag.IMMEDIATE_W_ENQUEUE) {
            try {
                // For now, the only supported transport is email
                sendEmail(toAddress, cc, bcc, fromAddress, fromName, subject, content, replyTo, attachementsFileNames,
                        messageType, messageTransport);
                status = Msg.Status.SENT;
            } catch (Throwable t) {
                // If something fails, capture error message
                status = Msg.Status.ERROR;
                error = t.toString();

                // Log exception only
                Logger.error("Exception while sending emails", t);

            }
        }

        // Persist message to retry later
        if (procFlag == ProcFlag.IMMEDIATE_W_ENQUEUE || procFlag == ProcFlag.ENQUEUE) {

            saveMessage(recipient, emailType, toAddress, cc, bcc, fromAddress, fromName, subject, content, replyTo,
                    attachementsFileNames, messageType, messageTransport, sentDate, status, error);
        }
    }

    //------------------------------------------------------------------------------------------------ 
    /**
     * Creates and sends a message using the provided transport. If something
     * fails, exceptions will be thrown. Note: this function does not perform
     * any checking on the target address nor it normalizes it.
     */
    private static void sendEmail(String toAddress, String cc, String bcc, String fromAddress, String fromName,
            String subject, String content, String replyTo, List<String> attachementsFileNames, Msg.Type messageType,
            Msg.Transport messageTransport) throws MessagingException {

        // Check if email sending is enabled
        if (!EMAIL_SENDING_ENABLE) {
            Logger.warn("Not sending email to: " + toAddress + ". Email sending is disabled");
            return;
        }

        // Check if can send emails to target address to do will add code here based on the instance
        //        if (!LCPlatformMode.isLcProd()
        //                && !(toAddress.endsWith("@sinolending.com") || toAddress.endsWith("@dianrong.com")
        //                        || toAddress.endsWith("@sl.com") || toAddress.endsWith("@scaleoptions.com"))) {
        //            logger.warning("Not sending email to: " + toAddress + ". Prod mode is disabled");
        //            return;
        //        }

        Session session = getSession(toAddress, messageType, messageTransport);
        Message msg = new MimeMessage(session);

        // Set to addresses
        Address[] addrs = InternetAddress.parse(toAddress);
        msg.addRecipients(Message.RecipientType.TO, addrs);
        // Set subject
        msg.setSubject(subject);
        // If provided, set from address and from name
        if (fromAddress != null) {
            try {
                msg.setFrom(new InternetAddress(fromAddress, fromName, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                // Should never happen
                System.out.println("Unsupported encoding");
            }
        }
        // If provided, set cc addresses
        if (cc != null) {
            addrs = InternetAddress.parse(cc);
            msg.addRecipients(Message.RecipientType.CC, addrs);
        }
        // If provided, set bcc addresses
        if (bcc != null) {
            addrs = InternetAddress.parse(bcc);
            msg.addRecipients(Message.RecipientType.BCC, addrs);
        }
        // If provided, set reply to address
        if (replyTo != null) {
            Address[] replyToAddress = { new InternetAddress(replyTo) };
            msg.setReplyTo(replyToAddress);
        }

        // Set headers
        msg.setHeader("X-Mailer", "JavaMailer");
        // Set sent date
        msg.setSentDate(Calendar.getInstance().getTime());

        // Set content
        if (attachementsFileNames == null || attachementsFileNames.size() == 0) {
            // Send mail without any attachements
            msg.setContent(content, "text/html; charset=UTF-8");

        } else {
            // Send mail with attachements
            MimeBodyPart messagePart = new MimeBodyPart();
            messagePart.setText(content);
            messagePart.setHeader("Content-Type", "text/html; charset=UTF-8");

            Multipart multipart = new MimeMultipart();
            multipart.addBodyPart(messagePart);

            String basePath = Constants.CONTENT_ROOT + "/emailTemplate/attachments/";

            // Add all attachements to mail content
            for (String fileName : attachementsFileNames) {
                MimeBodyPart attachmentPart = new MimeBodyPart();

                FileDataSource fileDataSource = new FileDataSource(basePath + fileName) {
                    @Override
                    public String getContentType() {
                        return "application/octet-stream";
                    }
                };

                attachmentPart.setDataHandler(new DataHandler(fileDataSource));
                attachmentPart.setFileName(fileName);

                multipart.addBodyPart(attachmentPart);
            }

            msg.setContent(multipart);
        }

        Transport.send(msg);

    }

    //------------------------------------------------------------------------------------------------ 
    /**
     * Get an email session based on the message and transport type
     * 
     * @param toAddress
     * @param type
     * @param transport
     * @return
     */
    private static Session getSession(String toAddress, Msg.Type type, Msg.Transport transport) {

        // Get SMTP server and port
        String smtpServer = getSMTPServer(toAddress, type, transport);

        // Return cached session
        if (sessions.get(smtpServer) != null) {
            return sessions.get(smtpServer);
        }

        // Create new session and keep in cache
        String[] parts = smtpServer.split(":");
        String host = parts[0];
        String port = parts.length > 1 ? parts[1] : SMTP_PORT;

        // Set session properties
        Properties props = new Properties();
        props.setProperty("mail.smtp.host", host);
        props.setProperty("mail.smtp.port", port);
        props.setProperty("mail.smtp.auth", "true");
        props.setProperty("mail.smtp.timeout", "" + SMTP_TIMEOUT * 10);
        props.setProperty("mail.smtp.connectiontimeout", "" + SMTP_TIMEOUT);
        props.setProperty("mail.smtp.ssl.enable", port.equals("465") ? "true" : "false"); // Enable SSL connection

        Session session = Session.getInstance(props, new Authenticator() {
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(SMTP_USERNAME, SMTP_PASSWORD);
            }
        });
        session.setDebug(false);

        // Cache session
        sessions.put(smtpServer, session);

        return session;
    }

    //------------------------------------------------------------------------------------------------ 
    /**
     * Determine which SMTP server to use depending on recipient email address
     */
    private static String getSMTPServer(String toAddress, Msg.Type type, Msg.Transport transport) {
        // Use internal SMTP server for our domains
        if (toAddress != null
                && (toAddress.endsWith("sinolending.com") || toAddress.endsWith("scaleoptions.com") || toAddress
                        .endsWith("dianrong.com")))
            return SMTP_SERVER + ":" + SMTP_PORT;
        return SMTP_SERVER + ":" + SMTP_PORT;
    }

    /**
     * Save a message in the queue
     */
    private static void saveMessage(ActorService recipient, EmailType emailType, String toAddress, String cc,
            String bcc, String fromAddress, String fromName, String subject, String content, String replyTo,
            List<String> attachementsFileNames, Msg.Type messageType, Msg.Transport messageTransport, Date sentDate,
            Msg.Status status, String error) {

        Msg msg = new Msg();
        try {
            JPAUtil.beginTransaction();
            if (recipient != null && recipient.getDBActor().getId() != null)
                msg.setToAid(recipient.getDBActor().getId());
            msg.setToAddr(toAddress);
            msg.setCc(cc);
            msg.setBcc(bcc);

            msg.setFromAddr(fromAddress);
            msg.setFromName(fromName);
            msg.setReplyTo(replyTo);

            // Create message content
            MsgDetail msgContent = new MsgDetail();
            msgContent.setSubject(subject);
            msgContent.setMessage(content);
            JPAUtil.em().persist(msgContent);
            msg.setContentId(msgContent.getId());
            
            msg.setType(messageType);
            msg.setTransport(messageTransport);
            msg.setTryCount(EMAIL_MAX_TRY_CNT);
            msg.setSentD(sentDate);

            msg.setStatus(status);

            if (error != null) {
                msg.appendError(error);
            }
            JPAUtil.em().merge(msg);
            JPAUtil.commitTransaction();
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            JPAUtil.closeEM();
        }
    }
}
